iT邦幫忙

2025 iThome 鐵人賽

DAY 24
0
Mobile Development

Flutter :30天打造念佛App,跨平台應用從Mobile到VR,讓極樂世界在眼前實現!系列 第 24

[ Day 24 ] Flutter 多國語系 — App 翻譯蒟蒻, 上架各國必備的好幫手!

  • 分享至 

  • xImage
  •  

2025 iThome鐵人賽
「 Flutter :30天打造念佛App,跨平台從Mobile到VR,讓極樂世界在眼前實現 ! 」
Day 24
「 Flutter 多國語系 — App 翻譯蒟蒻, 上架各國必備的好幫手 !


前言

昨天我們已經完成「第三方登入 Google & Apple 」,
我們已經可以辨識使用者身份,並且妥善地將使用者資料保存本機與雲端。
今天我們要來實作「多國語系」,
讓全球各地的使用者都能使用熟悉的語言操作App

Day24 文章目錄:
一、多國語系
二、前置設定
三、實作核心


一、多國語系

1. 簡介

多國語系通常是指為app添加語言轉換的功能,
其中會涉及兩個層面「國際化」、「本地化」。

2. 國際化與本地化

國際化( i18n ):指的是開發時將 App 設計成「 能夠適配 語言及文化轉換 」 。
例如:

  1. 字串不被截斷
  2. 兼顧方向性 (LTR/RTL)

本地化( l10n ):指的是將 App「 實際適配 」某個語言、地區與文化。
例如:

  1. 佛號累計108次,英文介面完整顯示:
    A total of 108 times of chanting Buddha's name
  2. 確保文字依照正確閱讀方向顯示:
    阿拉伯文的書寫閱讀由右至左
面向 i18n(工程能力) l10n(內容落地)
文案/字串 gen-l10n + ARB + 型別安全 AppLocalizations 為每個語言新增 app_xx.arb 並翻譯
框架接線 AppLocalizations.localizationsDelegates、supportedLocales 在 MaterialApp.locale 或 Localizations.override 覆蓋語系
日期/數字/貨幣 intl 的 DateFormat / NumberFormat 設定正確的 locale 與符號(例如 NT$)
方向(LTR/RTL) TextAlign.start/end、EdgeInsetsDirectional、AlignmentDirectional、Image.matchTextDirection 實機驗證 RTL、必要時局部 Directionality 覆蓋

3. 常見實作與擴充方式

類型 名稱 主要用途 優點 注意事項
官方流程 gen-l10n + flutter_localizations 以 ARB 產生 AppLocalizations,官方推薦做法 效能佳、與框架整合深 pubspec.yaml 啟用 flutter: generate: true
格式化 intl(Dart 套件) 日期/數字/貨幣/格式化 API 完整、跨平台 正確設定 locale 或使用 defaultLocale。
VS Code Extension ARB Editor ARB 語法高亮、驗證、片段 編輯體驗好、減少格式錯誤 僅編輯輔助
第三方封裝 easy_localization 以 JSON/鍵值快速上手的封裝 上手快、社群多示例 注意長期維護策略

二、前置設定

1. 安裝套件 pubspec.yaml

environment:
  sdk: ">=3.5.0 <4.0.0"

dependencies:
  flutter:
    sdk: flutter
  flutter_localizations:
    sdk: flutter
  intl: ^0.20.2

flutter:                     # 專案 flutter 設定區
  generate: true             # 必填,啟用官方 gen-l10n 產生器

2. 新增 l10n 設定 amitabha/l10n.yaml(新建在專案根目錄)

arb-dir: lib/l10n 
template-arb-file: app_en.arb
output-localization-file: app_localizations.dart

# 輸出到專案中,方便 IDE 導覽與相對匯入
synthetic-package: false
output-dir: lib/l10n/generated

# 常用選項(依需求開啟)
untranslated-messages-file: lib/l10n/untranslated.txt
nullable-getter: false
preferred-supported-locales: #限定與排序 supportedLocales
  - en
  - zh_Hant_TW

use-deferred-loading: false  #是否延遲載入

3. 建立ARB字串檔

zh_Hant_TW(中文.繁體.台灣)= 語言 language + 腳本 script + 地區 region ;

gen-l10n 規則:
帶腳本碼/國碼的語系,建議提供同檔名前綴的基底語言檔,作為回退階層的一環。
app_zh_Hant_TW.arb → app_zh_Hant.arb → app_zh.arb → template (app_en.arb)

(1) 建立基底語言檔 lib/l10n/app_zh.arb

{ "@@locale": "zh" }

(2) 建立基底語言檔 lib/l10n/app_zh_Hant.arb

{ "@@locale": "zh_Hant" }

(3) 建立字串檔 lib/l10n/app_zh_Hant_TW.arb

{
  "amitabha": "阿彌陀佛",
  "@amitabha": { "description": "佛號顯示文字" },

  "start": "開始",
  "@start": { "description": "開始按鈕" },

  "pause": "暫停",
  "@pause": { "description": "暫停按鈕" },

  "save": "儲存",
  "@save": { "description": "儲存按鈕" },

  "logIn": "登入",
  "@logIn": { "description": "登入按鈕" },
  
  "logOut": "登出",
  "@logOut": { "description": "登出按鈕" },

  "total": "累計",
  "@total": { "description": "統計標題" },

  "totalCount": "累計次數:{count}",
  "@totalCount": {
    "description": "帶數值的累計次數",
    "placeholders": { "count": { "type": "int", "example": "108" } }
  }
}

(4) 建立字串檔 lib/l10n/app_en.arb

{
  "amitabha": "Amitabha",
  "@amitabha": { "description": "Buddha name label" },

  "start": "Start",
  "@start": { "description": "Start button" },

  "pause": "Pause",
  "@pause": { "description": "Pause button" },

  "save": "Save",
  "@save": { "description": "Save button" },

  "logIn": "Log in",
  "@logIn": { "description": "Sign-in button" },
  
  "logOut": "Log Out",
  "@logOut": { "description": "Sign-out button" },

  "total": "Total",
  "@total": { "description": "Summary label (text only)" },

  "totalCount": "Total count: {count}",
  "@totalCount": {
    "description": "Total count with value",
    "placeholders": { "count": { "type": "int", "example": "108" } }
  }
}

2. 產生在地化類別 lib/l10n/generated/app_localizations.dart

終端機執行

flutter clean
flutter pub get
flutter gen-l10n     #會在 build/run 時自動產生,也可手動

三、實作核心

1. MaterialApp 接上本地化

//app.dart

// 匯入
import 'l10n/generated/app_localizations.dart';

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MultiProvider(
      providers: [
        ChangeNotifierProvider(create: (_) {
          final m = DownloadModel();
          if (F.appFlavor == Flavor.dev) {
            m.useAsr();
          } else {
            m.useKws();
          }
          return m;
        }),
        Provider<AuthFacade>(
          create: (_) => AuthFacade(
            auth: FirebaseAuthRepository(),
            users: FirestoreUserRepository(),
          ),
        ),
      ],
      child: MaterialApp(
        // 動態標題:多語自動顯示「阿彌陀佛 / Amitabha」
        onGenerateTitle: (ctx) => AppLocalizations.of(ctx)!.amitabha,

        theme: ThemeData(primarySwatch: Colors.blue),

        // 啟用框架內建字串與自訂翻譯
        localizationsDelegates: AppLocalizations.localizationsDelegates,
        supportedLocales: AppLocalizations.supportedLocales,
        home: _flavorBanner(child: const MyHomePage(), show: true),
      ),
    );
  }

  Widget _flavorBanner({required Widget child, bool show = true}) =>
      show ? Banner(
        location: BannerLocation.topStart,
        message: F.name,
        color: Colors.green.withAlpha(150),
        textStyle: const TextStyle(fontWeight: FontWeight.w700, fontSize: 12, letterSpacing: 1),
        textDirection: TextDirection.ltr,
        child: child,
      ) : child;
}
// my_home_page.dart
import 'package:flutter/material.dart';
import 'package:amitabha/flavors.dart';
import 'package:amitabha/streaming_asr.dart';
import 'package:amitabha/streaming_kws.dart';
import 'package:amitabha/l10n/generated/app_localizations.dart'; 

class MyHomePage extends StatelessWidget {
  const MyHomePage({super.key});

  @override
  Widget build(BuildContext context) {
    final t = AppLocalizations.of(context)!;   // 取得翻譯器
    final screen = (F.appFlavor == Flavor.dev)
        ? const StreamingAsrScreen()
        : const StreamingKwsScreen();

    return Scaffold(
      appBar: AppBar(
        title: Text(t.amitabha),    //阿彌陀佛 / Amitabha
        actions: [
          PopupMenuButton<String>(                      
            itemBuilder: (_) => [  //其他鍵值
              PopupMenuItem(value: 'start', child: Text(t.start)),
              PopupMenuItem(value: 'pause', child: Text(t.pause)),
              PopupMenuItem(value: 'save', child: Text(t.save)),
              PopupMenuItem(value: 'logIn', child: Text(t.logIn)),
              PopupMenuItem(value: 'logOut', child: Text(t.logOut)),
            ],
          ),
        ],
      ),
      body: screen,
    );
  }
}

2. 日期 / 數字 / 貨幣 :使用 intl

import 'package:intl/intl.dart';

//指定locale 
final dateStr = DateFormat.yMMMMEEEEd('zh_Hant_TW').format(DateTime.now());

//使用系統預設
String formatFullDate(BuildContext context, DateTime dt) {
  final locale = Localizations.localeOf(context).toString(); 
  return DateFormat.yMMMMEEEEd(locale).format(dt);
}

3. 多國語言 App 名稱

  • iOS

(1) Xcode Info 添加 Localizations : English 、Chinese (Traditional)

(2) 建立 InfoPlist.strings

//終端機執行
//移動到 iOS Runner 目錄
cd ios/Runner                     

//建立英文與繁體語系資料夾
mkdir -p en.lproj zh-Hant.lproj  

//建立英文版 InfoPlist.strings
cat > en.lproj/InfoPlist.strings <<'EOF'
"CFBundleDisplayName" = "Amitabha"; //App名稱
EOF

//建立繁體版 InfoPlist.strings
cat > zh-Hant.lproj/InfoPlist.strings <<'EOF'
"CFBundleDisplayName" = "念佛"; //App名稱
EOF

(3) 添加至專案

  • Android

(1) 確認 App 名稱不是硬寫常值 android/app/src/main/AndroidManifest.xml

//android/app/src/main/AndroidManifest.xml 

<application
    android:label="@string/app_name"   //字串資源才可以被本地化覆寫
    ... />

(2) 建立 strings.xml

//終端機執行
//移動到 android 目錄
cd android                    

//建立英文與繁體語系資料夾
mkdir -p app/src/main/res/values app/src/main/res/values-zh-rTW

//建立英文版strings.xml
cat > app/src/main/res/values/strings.xml <<'EOF'
<resources>
  <string name="app_name">Amitabha</string> //App名稱
</resources>
EOF

//建立中文版strings.xml
cat > app/src/main/res/values-zh-rTW/strings.xml <<'EOF'
<resources>
  <string name="app_name">念佛</string>     //App名稱
</resources>
EOF


Day24 重點回顧

重點 內容
多國語系 App 落實多語言文化
前置設定 安裝套件 ⭢ l10n 設定 ⭢ ARB字串 ⭢ 產生在地化類別
實作核心 MaterialApp 接上本地化

上一篇
[ Day 23 ] Flutter 第三方登入 實戰應用篇—穿越到Coding世界的勇者啊,你知道裝備可以放哪嗎(3) #Apple登入 #Google登入
下一篇
[ Day 25 ] Flutter UX & UI 實戰入門篇 — 生活在地球的勇者啊,極樂世界什麼樣子呀?(1)
系列文
Flutter :30天打造念佛App,跨平台應用從Mobile到VR,讓極樂世界在眼前實現!32
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言